/*
 * Copyright (c) 2018, apexes.net. All rights reserved.
 *
 *         http://www.apexes.net
 *
 */
package net.apexes.commons.net;

import net.apexes.commons.lang.Checks;
import net.apexes.commons.lang.Networks;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * Http文件上传
 * 
 * @author <a href="mailto:hedyn@foxmail.com">HeDYn</a>
 *
 */
public class HttpFileUploader {

    private static final String BOUNDARY = "---------7123a85b75f";

    private final String url;
    private final Map<String, String> httpProperties;
    private final Map<String, String> textParams;
    private final Map<String, FileParameter> fileParams;
    
    private int connectTimeout = 6 * 1000;
    private int readTimeout = 10 * 1000;

    private SSLContext sslContext;
    private HostnameVerifier hostNameVerifier;
                                     
    public HttpFileUploader(String url) {
        this.url = url;
        httpProperties = new LinkedHashMap<>();
        textParams = new HashMap<>();
        fileParams = new HashMap<>();
    }
    
    /**
     * 设置连接超时时间
     * @param timeout 超时时间，单位ms
     */
    public void setConnectTimeout(int timeout) {
        this.connectTimeout = timeout;
    }
    
    /**
     * 设置读取超时时间
     * @param timeout 超时时间，单位ms
     */
    public void setReadTimeout(int timeout) {
        this.readTimeout = timeout;
    }
    
    public void setHttpProperty(String key, String value) {
        Checks.verifyNotNull(key, "key");
        Checks.verifyNotNull(value, "value");
        httpProperties.put(key, value);
    }

    public void setSslContext(SSLContext sslContext) {
        this.sslContext = sslContext;
    }

    public void setHostNameVerifier(HostnameVerifier hostNameVerifier) {
        this.hostNameVerifier = hostNameVerifier;
    }

    /**
     * 增加一个普通字符串数据到表单数据中
     * @param name 名称
     * @param value 值
     */
    public void addTextParameter(String name, String value) {
        textParams.put(name, value);
    }
    
    /**
     * 增加一个文件到表单数据中
     * @param name 名称
     * @param file 文件
     */
    public void addFileParameter(String name, File file) {
        addFileParameter(name, file, "application/octet-stream");
    }

    /**
     * 增加一个文件到表单数据中
     * @param name 名称
     * @param file 文件
     * @param contentType 文件类型
     */
    public void addFileParameter(String name, File file, String contentType) {
        fileParams.put(name, new LocalFileParameter(file, contentType));
    }

    /**
     * 增加一个文件内容到表单数据中
     * @param name 名称
     * @param content 内容
     */
    public void addFileParameter(String name, byte[] content) {
        addFileParameter(name, name, content);
    }

    /**
     * 增加一个文件内容到表单数据中
     * @param name 名称
     * @param filename 文件名
     * @param content 内容
     */
    public void addFileParameter(String name, String filename, byte[] content) {
        addFileParameter(name, filename, content, "application/octet-stream");
    }

    /**
     * 增加一个文件内容到表单数据中
     * @param name 名称
     * @param content 内容
     * @param contentType 类型
     */
    public void addFileParameter(String name, byte[] content, String contentType) {
        addFileParameter(name, name, content, contentType);
    }

    /**
     * 增加一个文件内容到表单数据中
     * @param name 名称
     * @param filename 文件名
     * @param content 内容
     * @param contentType 类型
     */
    public void addFileParameter(String name, String filename, byte[] content, String contentType) {
        fileParams.put(name, new BytesFileParameter(filename, content, contentType));
    }
    
    /**
     * 清除所有表单数据
     */
    public void clearParameter() {
        textParams.clear();
        fileParams.clear();
    }
    
    public byte[] upload() throws Exception {
        HttpURLConnection conn = Networks.connect(new URL(url), sslContext, hostNameVerifier);
        conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + BOUNDARY);
        conn.setRequestMethod("POST");
        conn.setDoOutput(true);
        conn.setUseCaches(false);
        conn.setConnectTimeout(connectTimeout);
        conn.setReadTimeout(readTimeout);

        for (Map.Entry<String, String> entry : httpProperties.entrySet()) {
            conn.setRequestProperty(entry.getKey(), entry.getValue());
        }

        try {
            conn.connect();

            DataOutputStream ops = new DataOutputStream(conn.getOutputStream());
            writeFileParams(ops);
            writeTextParams(ops);
            
            ops.writeBytes("--" + BOUNDARY + "--" + "\r\n");
            ops.writeBytes("\r\n");
            ops.flush();
            
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            InputStream in = conn.getInputStream();
            byte[] buf = new byte[1024];
            int len;
            while ((len = in.read(buf)) != -1) {
                out.write(buf, 0, len);
            }
            return out.toByteArray();
        } finally {
            conn.disconnect();
        }
    }
    
    private void writeTextParams(DataOutputStream ops) throws Exception {
        for (Map.Entry<String, String> entry : textParams.entrySet()) {
            String name = entry.getKey();
            String value = entry.getValue();
            ops.writeBytes("--" + BOUNDARY + "\r\n");
            ops.writeBytes("Content-Disposition: form-data; name=\"" + name + "\"\r\n");
            ops.writeBytes("\r\n");
            ops.writeBytes(URLEncoder.encode(value, "UTF-8") + "\r\n");
        }
    }

    private void writeFileParams(DataOutputStream ops) throws Exception {
        for (Map.Entry<String, FileParameter> entry : fileParams.entrySet()) {
            String name = entry.getKey();
            FileParameter param = entry.getValue();
            ops.writeBytes("--" + BOUNDARY + "\r\n");
            ops.writeBytes("Content-Disposition: form-data; name=\"" + name + "\"; filename=\""
                    + URLEncoder.encode(param.getFilename(), "UTF-8") + "\"\r\n");
            ops.writeBytes("Content-Type: " + param.getContentType() + "\r\n");
            ops.writeBytes("\r\n");
            ops.write(param.getContent());
            ops.writeBytes("\r\n");
        }
    }
    
    /**
     * 
     * @author <a href="mailto:hedyn@foxmail.com">HeDYn</a>
     *
     */
    public interface FileParameter {
        
        String getContentType();
        
        String getFilename();
        
        byte[] getContent() throws Exception;
        
    }
    
    /**
     * 
     * @author <a href="mailto:hedyn@foxmail.com">HeDYn</a>
     *
     */
    private static class LocalFileParameter implements FileParameter {
        
        private final File file;
        private final String contentType;
        
        private LocalFileParameter(File file, String contentType) {
            this.file = file;
            this.contentType = contentType;
        }

        @Override
        public String getContentType() {
            return contentType;
        }

        @Override
        public String getFilename() {
            return file.getName();
        }

        @Override
        public byte[] getContent() throws Exception {
            ByteArrayOutputStream ops = new ByteArrayOutputStream();
            try (FileInputStream in = new FileInputStream(file)) {
                byte[] buf = new byte[1024];
                int len;
                while ((len = in.read(buf)) != -1) {
                    ops.write(buf, 0, len);
                }
            }
            return ops.toByteArray();
        }
    }
    
    /**
     * 
     * @author <a href="mailto:hedyn@foxmail.com">HeDYn</a>
     *
     */
    private static class BytesFileParameter implements FileParameter {

        private final String filename;
        private final byte[] content;
        private final String contentType;
        
        private BytesFileParameter(String filename, byte[] content, String contentType) {
            this.filename = filename;
            this.content = content;
            this.contentType = contentType;
        }

        @Override
        public String getContentType() {
            return contentType;
        }

        @Override
        public String getFilename() {
            return filename;
        }

        @Override
        public byte[] getContent() throws Exception {
            return content;
        }
        
    }

}
